﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Diagnostics;

namespace Xceed.Silverlight.Data
{
  internal sealed class GroupDataHelper : ContextualParentDataHelper, IContextualGroupDataHelper
  {
    #region Constructor

    internal GroupDataHelper( IContextualDataHelper childDataHelper )
      : base( childDataHelper )
    {
    }

    #endregion

    private int CompareHeadersFooters( DataSourceContext context, DataPath firstItem, DataPath secondItem )
    {
      Debug.Assert( object.Equals( firstItem.ParentPath, secondItem.ParentPath ) );

      DataPath groupPath = firstItem.ParentPath;
      HeaderFooterDataItem firstItemHeaderFooterDataItem = ( HeaderFooterDataItem )firstItem.LastChild;
      HeaderFooterDataItem secondItemHeaderFooterDataItem = ( HeaderFooterDataItem )secondItem.LastChild;

      Debug.Assert( firstItemHeaderFooterDataItem.IsHeader == secondItemHeaderFooterDataItem.IsHeader );

      object firstHeaderFooterValue = firstItemHeaderFooterDataItem.Data;
      object secondHeaderFooterValue = secondItemHeaderFooterDataItem.Data;

      if( firstHeaderFooterValue == secondHeaderFooterValue )
        return 0;

      IGroupHeadersFootersSelector selector = context.GroupContext.GroupHeadersFootersSelector;

      if( selector == null )
        throw new InvalidOperationException( "Cannot compare headers and footers." );

      IEnumerable<object> headersFooters = ( firstItemHeaderFooterDataItem.IsHeader )
                                             ? selector.GetHeaders( groupPath )
                                             : selector.GetFooters( groupPath );



      foreach( object item in headersFooters )
      {
        if( firstHeaderFooterValue == item )
          return -1;

        if( secondHeaderFooterValue == item )
          return 1;
      }

      throw new InvalidOperationException( "The header or footer was not found." );
    }

    private SortPosition CreateSortPosition( DataSourceContext context, DataPath item, DataPathType itemType )
    {
      if( item == null )
        throw new ArgumentNullException( "item" );

      if( itemType == DataPathType.Group
        || itemType == DataPathType.Item )
      {
        return this.CreateSortPosition( context, item, StartPosition.Before );
      }
      else if( itemType == DataPathType.HeaderFooter )
      {
        DataPath parentPath = item.CreateAncestorPath( item.Depth - 1 );
        StartPosition startPosition = GroupDataHelper.GetStartPosition( item );

        return this.CreateSortPosition( context, parentPath, startPosition );
      }

      throw new DataInternalException();
    }

    private IEnumerable<object> GetSortValues( DataSourceContext context, IEnumerable<string> propertyPaths, DataPath item )
    {
      foreach( string propertyPath in propertyPaths )
      {
        object value;

        if( !this.TryGetMemberValue( context, propertyPath, item, out value ) )
          break;

        yield return value;
      }
    }

    private bool IsGroupHeaderFooterPath( DataSourceContext context, DataPath item )
    {
      if( context == null )
        throw new ArgumentNullException( "context" );

      if( item == null )
        throw new ArgumentNullException( "item" );

      int itemDepth = item.Depth;
      int groupDepth = itemDepth - 1;

      if( ( itemDepth <= 0 ) || ( !( item[ groupDepth ] is HeaderFooterDataItem ) ) )
        return false;

      return GroupDataHelper.IsGroupPath( context.GroupContext, item, groupDepth );
    }

    private DataPathType GetDataPathType( DataSourceContext context, DataPath item )
    {
      if( this.IsGroupPath( context, item ) )
        return DataPathType.Group;

      if( this.IsGroupHeaderFooterPath( context, item ) )
        return DataPathType.HeaderFooter;

      return DataPathType.Item;
    }

    private bool IsGroupRelatedPath( DataSourceContext context, DataPath item )
    {
      DataPathType type = this.GetDataPathType( context, item );

      return GroupDataHelper.IsGroupRelatedPath( type );
    }

    private static bool IsGroupPath( DataSourceGroupContext context, DataPath item, int depth )
    {
      if( context == null )
        throw new ArgumentNullException( "context" );

      if( item == null )
        throw new ArgumentNullException( "item" );

      int groupDepth = Math.Min( item.Depth, depth );

      if( groupDepth > 0 )
      {
        int index = groupDepth - 1;

        // Right now, since there is no other type of DataItem that could
        // exist before a GroupDataItem, we know the item targets a group
        // at this level if it is a GroupDataItem.  In the future, if another
        // type of DataItem could exist before a GroupDataItem, we'll have
        // to revised this method in order to make sure it return true only
        // when the DataPath from level 1 to "depth" targets a group.
        if( item[ index ] is GroupDataItem )
          return true;

        // The grouping of the item doesn't match the grouping of the context.
        if( ( groupDepth <= context.GroupDescriptions.Count )
          && ( !( item[ index ] is HeaderFooterDataItem ) ) )
          throw new DataInternalException();
      }

      return false;
    }

    private static bool IsGroupRelatedPath( DataPathType dataPathType )
    {
      return ( dataPathType != DataPathType.Item );
    }

    private static StartPosition GetStartPosition( DataPath item )
    {
      if( item == null )
        throw new ArgumentNullException( "item" );

      bool isHeader = ( ( HeaderFooterDataItem )item.LastChild ).IsHeader;

      return ( isHeader ) ? StartPosition.Before : StartPosition.After;
    }

    #region IContextualItemHelper<DataPath> Members

    public override object GetMemberValue( DataSourceContext context, string propertyPath, DataPath item )
    {
      object value;

      if( !this.TryGetMemberValue( context, propertyPath, item, out value ) )
        throw new ArgumentException( "The specified member was not found on the item.", "propertyPath" );

      return value;
    }

    public override bool TryGetMemberValue( DataSourceContext context, string propertyPath, DataPath item, out object value )
    {
      if( context == null )
        throw new ArgumentNullException( "context" );

      if( item == null )
        throw new ArgumentNullException( "item" );

      DataSourceGroupContext groupContext = context.GroupContext;
      int groupCount = groupContext.GroupDescriptions.Count;
      int index = 0;
      bool indexFound = false;

      if( groupCount > 0 )
      {
        foreach( string groupName in groupContext.GroupNames )
        {
          if( groupName == propertyPath )
          {
            indexFound = true;
            break;
          }

          index++;
        }
      }

      if( ( indexFound ) && ( index < item.Depth ) )
      {
        GroupDataItem groupDataItem = item[ index ] as GroupDataItem;

        if( groupDataItem != null )
        {
          value = groupDataItem.Data;
          return true;
        }
      }

      if( this.IsGroupRelatedPath( context, item ) )
      {
        value = null;
        return false;
      }

      return base.TryGetMemberValue( context, propertyPath, item, out value );
    }

    #endregion

    #region IContextualConvertHelper<DataPath, DataPath> Members

    public override DataPath Convert( DataSourceContext context, DataPath value )
    {
      if( context == null )
        throw new ArgumentNullException( "context" );

      if( value == null )
        throw new ArgumentNullException( "value" );

      DataSourceGroupContext groupContext = context.GroupContext;

      // There is nothing to do if there is no grouping at all.
      if( !groupContext.AnyGroup )
        return base.Convert( context, value );

      IEnumerable<DataItem> dataItems = ( from propertyPath in groupContext.GroupNames
                                          let groupValue = this.ChildDataHelper.GetMemberValue( context, propertyPath, value )
                                          select new GroupDataItem( groupValue ) ).Cast<DataItem>();
      dataItems = dataItems.Concat( value.Path );

      return new DataPath( dataItems.ToArray(), true );
    }

    public override DataPath ConvertBack( DataSourceContext context, DataPath value )
    {
      if( context == null )
        throw new ArgumentNullException( "context" );

      if( value == null )
        throw new ArgumentNullException( "value" );

      if( ( value.LastChild is GroupDataItem ) || ( value.LastChild is HeaderFooterDataItem ) )
        return null;

      int groupCount = context.GroupContext.GroupDescriptions.Count;

      // There is nothing to do if there is no grouping at all.
      if( groupCount == 0 )
        return base.ConvertBack( context, value );

      if( value.Depth < groupCount + 1 )
        throw new DataInternalException( "The specified data path is invalid." );

      IEnumerable<DataItem> dataItems = value.Path.Skip( groupCount );

      return new DataPath( dataItems.ToArray(), true );
    }

    public override object DeepConvertBack( DataSourceContext context, DataPath value )
    {
      DataPath childItem = this.ConvertBack( context, value );

      // The path isn't targeting a header, a footer or a group.
      if( childItem != null )
        return this.ChildDataHelper.DeepConvertBack( context, childItem );

      // We cannot convert the path any further.  The value cannot be converted into
      // a user object.
      return null;
    }

    #endregion

    #region IContextualCompareHelper<DataPath> Members

    public override int Compare( DataSourceContext context, DataPath firstItem, DataPath secondItem )
    {
      if( object.ReferenceEquals( firstItem, secondItem ) )
        return 0;

      DataPathType firstItemType = this.GetDataPathType( context, firstItem );
      DataPathType secondItemType = this.GetDataPathType( context, secondItem );

      // The base implementation is able to compare DataPath with no group.
      if( !GroupDataHelper.IsGroupRelatedPath( firstItemType )
        && !GroupDataHelper.IsGroupRelatedPath( secondItemType ) )
        return base.Compare( context, firstItem, secondItem );

      // We cannot compare a group with a child of the same group.
      if( ( firstItemType == DataPathType.Group ) && ( firstItem.IsAncestorOf( secondItem ) ) )
        throw new InvalidOperationException( "Cannot compare a path with one of its descendants." );

      // We cannot compare a group with a child of the same group.
      if( ( secondItemType == DataPathType.Group ) && ( secondItem.IsAncestorOf( firstItem ) ) )
        throw new InvalidOperationException( "Cannot compare a path with one of its descendants." );

      SortPosition firstItemPosition = this.CreateSortPosition( context, firstItem, firstItemType );
      SortPosition secondItemPosition = this.CreateSortPosition( context, secondItem, secondItemType );

      int compare = this.Compare( context, firstItemPosition, secondItemPosition );
      if( compare != 0 )
        return compare;

      // The two items are both headers or footers of the same group.
      if( ( firstItemType == DataPathType.HeaderFooter ) && ( secondItemType == DataPathType.HeaderFooter ) )
        return this.CompareHeadersFooters( context, firstItem, secondItem );

      return 0;
    }

    public override int Compare( DataSourceContext context, DataPath item, SourcePosition position )
    {
      DataPathType itemType = this.GetDataPathType( context, item );
      SortPosition itemPosition = this.CreateSortPosition( context, item, itemType );

      return this.Compare( context, itemPosition, position );
    }

    #endregion

    #region IContextualEditHelper<DataPath> Members

    public override void Invalidate( DataSourceContext context, DataPath item )
    {
      DataPath childDataPath = this.ConvertBack( context, item );

      if( childDataPath != null )
      {
        this.ChildDataHelper.Invalidate( context, childDataPath );
      }
    }

    #endregion

    #region IContextualGroupHelper Members

    public bool IsGroupPath( DataSourceContext context, DataPath item )
    {
      if( context == null )
        throw new ArgumentNullException( "context" );

      if( item == null )
        throw new ArgumentNullException( "item" );

      return GroupDataHelper.IsGroupPath(
        context.GroupContext,
        item,
        item.Depth );
    }

    public DataPath GetGroupPath( DataSourceContext context, DataPath item )
    {
      if( context == null )
        throw new ArgumentNullException( "context" );

      if( item == null )
        throw new ArgumentNullException( "item" );

      int groupDepth = context.GroupContext.GroupDescriptions.Count;

      if( item.Depth < groupDepth )
        return item;

      return item.CreateAncestorPath( groupDepth );
    }

    public SortPosition CreateSortPosition(
      DataSourceContext context,
      DataPath item,
      StartPosition startPosition )
    {
      if( context == null )
        throw new ArgumentNullException( "context" );

      if( item == null )
        throw new ArgumentNullException( "item" );

      IEnumerable<SortDescription> currentCompleteSortOrder = context.GetCurrentCompleteSortOrder();
      List<SortDescription> sortOrder;

      if( item.Depth <= context.GroupContext.GroupDescriptions.Count )
      {
        sortOrder = currentCompleteSortOrder.Take( item.Depth ).ToList();
      }
      else
      {
        sortOrder = currentCompleteSortOrder.ToList();
      }

      IEnumerable<string> propertyPaths = sortOrder.Select( ( sd ) => sd.PropertyName );
      List<object> sortValues = this.GetSortValues( context, propertyPaths, item ).ToList();

      return new SortPosition( sortOrder, sortValues, startPosition );
    }

    #endregion

    #region DataPathType Nested Type

    private enum DataPathType : byte
    {
      Item = 0,
      Group,
      HeaderFooter
    }

    #endregion
  }
}
